/**
 * github : https://github.com/Carbs0126/NumberPickerView
 */

package cn.carbswang.ohos.numberpickerview.library;

import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.DragInfo;
import ohos.agp.components.ScrollHelper;
import ohos.agp.components.Text;
import ohos.agp.components.VelocityDetector;
import ohos.agp.components.element.Element;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.text.Font;
import ohos.agp.utils.Color;
import ohos.agp.utils.TextAlignment;
import ohos.agp.utils.TextTool;
import ohos.agp.window.service.DisplayManager;
import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.eventhandler.InnerEvent;
import ohos.multimodalinput.event.TouchEvent;
import ohos.utils.PacMap;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class NumberPickerView extends Component implements Component.EstimateSizeListener,
    Component.DrawTask,
    Component.TouchEventListener {
    // default text color of not selected item
    private static final int DEFAULT_TEXT_COLOR_NORMAL = 0XFF333333;

    // default text color of selected item
    private static final int DEFAULT_TEXT_COLOR_SELECTED = 0XFFF56313;

    // default text size of normal item
    private static final int DEFAULT_TEXT_SIZE_NORMAL_SP = 14;

    // default text size of selected item
    private static final int DEFAULT_TEXT_SIZE_SELECTED_SP = 16;

    // default text size of hint text, the middle item's right text
    private static final int DEFAULT_TEXT_SIZE_HINT_SP = 14;

    // distance between selected text and hint text
    private static final int DEFAULT_MARGIN_START_OF_HINT_DP = 8;

    // distance between hint text and right of this view, used in wrap_content mode
    private static final int DEFAULT_MARGIN_END_OF_HINT_DP = 8;

    // default divider's color
    private static final int DEFAULT_DIVIDER_COLOR = 0XFFF56313;

    // default divider's height
    private static final int DEFAULT_DIVIDER_HEIGHT = 2;

    // default divider's margin to the left & right of this view
    private static final int DEFAULT_DIVIDER_MARGIN_HORIZONTAL = 0;

    // default shown items' count, now we display 3 items, the 2nd one is selected
    private static final int DEFAULT_SHOWN_COUNT = 3;

    // default items' horizontal padding, left padding and right padding are both 5dp,
    // only used in wrap_content mode
    private static final int DEFAULT_ITEM_PADDING_DP_H = 5;

    // default items' vertical padding, top padding and bottom padding are both 2dp,
    // only used in wrap_content mode
    private static final int DEFAULT_ITEM_PADDING_DP_V = 2;

    // message's what argument to refresh current state, used by mHandler
    private static final int HANDLER_WHAT_REFRESH = 1;

    // message's what argument to respond value changed event, used by mHandler
    private static final int HANDLER_WHAT_LISTENER_VALUE_CHANGED = 2;

    // message's what argument to request layout, used by mHandlerInMainThread
    private static final int HANDLER_WHAT_REQUEST_LAYOUT = 3;

    // interval time to scroll the distance of one item's height
    private static final int HANDLER_INTERVAL_REFRESH = 32; // millisecond

    // in millisecond unit, default duration of scrolling an item' distance
    private static final int DEFAULT_INTERVAL_REVISE_DURATION = 300;

    // max and min durations when scrolling from one value to another
    private static final int DEFAULT_MIN_SCROLL_BY_INDEX_DURATION = DEFAULT_INTERVAL_REVISE_DURATION * 1;
    private static final int DEFAULT_MAX_SCROLL_BY_INDEX_DURATION = DEFAULT_INTERVAL_REVISE_DURATION * 2;

    private static final String TEXT_ELLIPSIZE_START = "start";
    private static final String TEXT_ELLIPSIZE_MIDDLE = "middle";
    private static final String TEXT_ELLIPSIZE_END = "end";

    private static final boolean DEFAULT_SHOW_DIVIDER = true;
    private static final boolean DEFAULT_WRAP_SELECTOR_WHEEL = true;
    private static final boolean DEFAULT_CURRENT_ITEM_INDEX_EFFECT = false;
    private static final boolean DEFAULT_RESPOND_CHANGE_ON_DETACH = false;
    private static final boolean DEFAULT_RESPOND_CHANGE_IN_MAIN_THREAD = true;

    private int mTextColorNormal = DEFAULT_TEXT_COLOR_NORMAL;
    private int mTextColorSelected = DEFAULT_TEXT_COLOR_SELECTED;
    private int mTextColorHint = DEFAULT_TEXT_COLOR_SELECTED;
    private int mTextSizeNormal = 0;
    private int mTextSizeSelected = 0;
    private int mTextSizeHint = 0;
    private int mWidthOfHintText = 0;
    private int mWidthOfAlterHint = 0;
    private int mMarginStartOfHint = 0;
    private int mMarginEndOfHint = 0;
    private int mItemPaddingVertical = 0;
    private int mItemPaddingHorizontal = 0;
    private int mDividerColor = DEFAULT_DIVIDER_COLOR;
    private int mDividerHeight = DEFAULT_DIVIDER_HEIGHT;
    private int mDividerMarginL = DEFAULT_DIVIDER_MARGIN_HORIZONTAL;
    private int mDividerMarginR = DEFAULT_DIVIDER_MARGIN_HORIZONTAL;
    private int mShownCount = DEFAULT_SHOWN_COUNT;
    private int mDividerIndex0 = 0;
    private int mDividerIndex1 = 0;
    private int mMinShowIndex = -1;
    private int mMaxShowIndex = -1;
    // compat for android.widget.NumberPicker
    private int mMinValue = 0;
    // compat for android.widget.NumberPicker
    private int mMaxValue = 0;
    private int mMaxWidthOfDisplayedValues = 0;
    private int mMaxHeightOfDisplayedValues = 0;
    private int mMaxWidthOfAlterArrayWithMeasureHint = 0;
    private int mMaxWidthOfAlterArrayWithoutMeasureHint = 0;
    private int mPrevPickedIndex = 0;
    private int mMiniVelocityFling = 150;
    private int mScaledTouchSlop = 8;
    private String mHintText;
    private String mTextEllipsize;
    private String mEmptyItemHint;
    private String mAlterHint;
    // friction used by scroller when fling
    private float mFriction = 1f;
    private float mTextSizeNormalCenterYOffset = 0f;
    private float mTextSizeSelectedCenterYOffset = 0f;
    private float mTextSizeHintCenterYOffset = 0f;
    // true to show the two dividers
    private boolean mShowDivider = DEFAULT_SHOW_DIVIDER;
    // true to wrap the displayed values
    private boolean mWrapSelectorWheel = DEFAULT_WRAP_SELECTOR_WHEEL;
    // true to set to the current position, false set position to 0
    private boolean mCurrentItemIndexEffect = DEFAULT_CURRENT_ITEM_INDEX_EFFECT;
    // true if NumberPickerView has initialized
    private boolean mHasInit = false;
    // if displayed values' number is less than show count, then this value will be false.
    private boolean mWrapSelectorWheelCheck = true;
    // if you want you set to linear mode from wrap mode when scrolling, then this value will be true.
    private boolean mPendingWrapToLinear = false;

    // if this view is used in same dialog or PopupWindow more than once, and there are several
    // NumberPickerViews linked, such as Gregorian Calendar with MonthPicker and DayPicker linked,
    // set mRespondChangeWhenDetach true to respond onValueChanged callbacks if this view is scrolling
    // when detach from window, but this solution is unlovely and may cause NullPointerException
    // (even i haven't found this NullPointerException),
    // so I highly recommend that every time setting up a reusable dialog with a NumberPickerView in it,
    // please initialize NumberPickerView's data, and in this way, you can set mRespondChangeWhenDetach false.
    private boolean mRespondChangeOnDetach = DEFAULT_RESPOND_CHANGE_ON_DETACH;

    // this is to set which thread to respond onChange... listeners including
    // OnValueChangeListener, OnValueChangeListenerRelativeToRaw and OnScrollListener when view is
    // scrolling or starts to scroll or stops scrolling.
    private boolean mRespondChangeInMainThread = DEFAULT_RESPOND_CHANGE_IN_MAIN_THREAD;

    // this drawable will draw for background for selected item
    private Element mSelectedItemBackground = null;

    private ScrollHelper mScroller;
    private VelocityDetector mVelocityTracker;

    private Paint mPaintDivider = new Paint();
    private TextPaint mPaintText = new TextPaint();
    private Paint mPaintHint = new Paint();

    private String[] mDisplayedValues;
    private CharSequence[] mAlterTextArrayWithMeasureHint;
    private CharSequence[] mAlterTextArrayWithoutMeasureHint;

    private HandlerThread mHandlerThread;
    private EventHandler mHandlerInNewThread;
    private EventHandler mHandlerInMainThread;

    private Map<String, Integer> mTextWidthCache = new ConcurrentHashMap<>();

    // compatible for NumberPicker
    public interface OnValueChangeListener {
        void onValueChange(NumberPickerView picker, int oldVal, int newVal);
    }

    public interface OnValueChangeListenerRelativeToRaw {
        void onValueChangeRelativeToRaw(NumberPickerView picker, int oldPickedIndex, int newPickedIndex,
                                        String[] displayedValues);
    }

    public interface OnValueChangeListenerInScrolling {
        void onValueChangeInScrolling(NumberPickerView picker, int oldVal, int newVal);
    }

    // compatible for NumberPicker
    public interface OnScrollListener {
        int SCROLL_STATE_IDLE = 0;
        int SCROLL_STATE_TOUCH_SCROLL = 1;
        int SCROLL_STATE_FLING = 2;

        void onScrollStateChange(NumberPickerView view, int scrollState);
    }

    private OnValueChangeListenerRelativeToRaw mOnValueChangeListenerRaw;
    private OnValueChangeListener mOnValueChangeListener; // compatible for NumberPicker
    private OnScrollListener mOnScrollListener; // compatible for NumberPicker
    private OnValueChangeListenerInScrolling mOnValueChangeListenerInScrolling; // response onValueChanged in scrolling

    // The current scroll state of the NumberPickerView.
    private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;

    public NumberPickerView(Context context) {
        this(context, null);
    }

    public NumberPickerView(Context context, AttrSet attrs) {
        this(context, attrs, 0);
    }

    public NumberPickerView(Context context, AttrSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttr(context, attrs);
        init(context);
        initListener();
    }

    private void initAttr(Context context, AttrSet attrs) {
        if (attrs == null) {
            return;
        }
        mShownCount = AttrValue.getPrimitive(attrs, "npv_ShownCount", DEFAULT_SHOWN_COUNT);
        mDividerColor = AttrValue.getPrimitive(attrs, "npv_DividerColor", DEFAULT_DIVIDER_COLOR);

        mDividerHeight = AttrValue.getDimension(attrs, "npv_DividerHeight", DEFAULT_DIVIDER_HEIGHT);
        mDividerMarginL = AttrValue.getDimension(attrs, "npv_DividerMarginLeft", DEFAULT_DIVIDER_MARGIN_HORIZONTAL);
        mDividerMarginR = AttrValue.getDimension(attrs, "npv_DividerMarginRight", DEFAULT_DIVIDER_MARGIN_HORIZONTAL);

        mDisplayedValues = AttrValue.getStringArray(getContext(), attrs, "npv_TextArray");
        mTextColorNormal = AttrValue.getPrimitive(attrs, "npv_TextColorNormal", DEFAULT_TEXT_COLOR_NORMAL);
        mTextColorSelected = AttrValue.getPrimitive(attrs, "npv_TextColorSelected", DEFAULT_TEXT_COLOR_SELECTED);

        mTextColorHint = AttrValue.getPrimitive(attrs, "npv_TextColorHint", DEFAULT_TEXT_COLOR_SELECTED);
        mTextSizeNormal = AttrValue.getDimension(attrs,
            "npv_TextSizeNormal", sp2px(context, DEFAULT_TEXT_SIZE_NORMAL_SP));
        mTextSizeSelected = AttrValue.getDimension(attrs,
            "npv_TextSizeSelected", sp2px(context, DEFAULT_TEXT_SIZE_SELECTED_SP));
        mTextSizeHint = AttrValue.getDimension(attrs, "npv_TextSizeHint", sp2px(context, DEFAULT_TEXT_SIZE_HINT_SP));

        mMinShowIndex = AttrValue.getPrimitive(attrs, "npv_MinValue", -1);
        mMaxShowIndex = AttrValue.getPrimitive(attrs, "npv_MaxValue", -1);
        mWrapSelectorWheel = AttrValue.getPrimitive(attrs, "npv_WrapSelectorWheel", DEFAULT_WRAP_SELECTOR_WHEEL);
        mShowDivider = AttrValue.getPrimitive(attrs, "npv_ShowDivider", DEFAULT_SHOW_DIVIDER);

        mHintText = AttrValue.getPrimitive(attrs, "npv_HintText", "");
        mAlterHint = AttrValue.getPrimitive(attrs, "npv_AlternativeHint", "");
        mEmptyItemHint = AttrValue.getPrimitive(attrs, "npv_EmptyItemHint", "");
        mMarginStartOfHint = AttrValue.getDimension(attrs,
            "npv_MarginStartOfHint", dp2px(context, DEFAULT_MARGIN_START_OF_HINT_DP));
        mMarginEndOfHint = AttrValue.getDimension(attrs,
            "npv_MarginEndOfHint", dp2px(context, DEFAULT_MARGIN_END_OF_HINT_DP));

        mItemPaddingVertical = AttrValue.getDimension(attrs,
            "npv_ItemPaddingVertical", dp2px(context, DEFAULT_ITEM_PADDING_DP_V));
        mItemPaddingHorizontal = AttrValue.getDimension(attrs,
            "npv_ItemPaddingHorizontal", dp2px(context, DEFAULT_ITEM_PADDING_DP_H));
        mAlterTextArrayWithMeasureHint = AttrValue.getStringArray(getContext(), attrs,
            "npv_AlternativeTextArrayWithMeasureHint");
        mAlterTextArrayWithoutMeasureHint = AttrValue.getStringArray(getContext(), attrs,
            "npv_AlternativeTextArrayWithoutMeasureHint");
        mRespondChangeOnDetach = AttrValue.getPrimitive(attrs,
            "npv_RespondChangeOnDetached", DEFAULT_RESPOND_CHANGE_ON_DETACH);
        mRespondChangeInMainThread = AttrValue.getPrimitive(attrs,
            "npv_RespondChangeInMainThread", DEFAULT_RESPOND_CHANGE_IN_MAIN_THREAD);
        mTextEllipsize = AttrValue.getPrimitive(attrs, "npv_TextEllipsize", "");
        mSelectedItemBackground = AttrValue.getElement(attrs, "npv_SelectedItemBackground");
    }

    private void initListener() {
        // 测量
        setEstimateSizeListener(this);
        // 绘制
        addDrawTask(this);
        // 触摸
        setTouchEventListener(this);
        setScrolledListener(new ScrolledListener() {
            @Override
            public void onContentScrolled(Component component, int i, int i1, int i2, int i3) {
            }
        });
        setComponentStateChangedListener(new ComponentStateChangedListener() {
            @Override
            public void onComponentStateChanged(Component component, int i) {
            }
        });
        setLayoutRefreshedListener(new LayoutRefreshedListener() {
            @Override
            public void onRefreshed(Component component) {
                onSizeChanged(component.getEstimatedWidth(), component.getEstimatedHeight());
                invalidate();
            }
        });
        setBindStateChangedListener(new BindStateChangedListener() {
            @Override
            public void onComponentBoundToWindow(Component component) {
                onAttachedToWindow();
            }

            @Override
            public void onComponentUnboundFromWindow(Component component) {
                onDetachedFromWindow();
            }
        });
        setDraggedListener(DRAG_VERTICAL, new DraggedListener() {
            @Override
            public boolean onDragPreAccept(Component component, int dragDirection) {
                return true;
            }

            @Override
            public void onDragDown(Component component, DragInfo dragInfo) {
            }

            @Override
            public void onDragStart(Component component, DragInfo dragInfo) {
            }

            @Override
            public void onDragUpdate(Component component, DragInfo dragInfo) {
            }

            @Override
            public void onDragEnd(Component component, DragInfo dragInfo) {
            }

            @Override
            public void onDragCancel(Component component, DragInfo dragInfo) {
            }
        });
    }

    private void init(Context context) {
        mScroller = new ScrollHelper();
        mMiniVelocityFling = 50;
        mScaledTouchSlop = 8;
        if (mTextSizeNormal == 0) {
            mTextSizeNormal = sp2px(context, DEFAULT_TEXT_SIZE_NORMAL_SP);
        }
        if (mTextSizeSelected == 0) {
            mTextSizeSelected = sp2px(context, DEFAULT_TEXT_SIZE_SELECTED_SP);
        }
        if (mTextSizeHint == 0) {
            mTextSizeHint = sp2px(context, DEFAULT_TEXT_SIZE_HINT_SP);
        }
        if (mMarginStartOfHint == 0) {
            mMarginStartOfHint = dp2px(context, DEFAULT_MARGIN_START_OF_HINT_DP);
        }
        if (mMarginEndOfHint == 0) {
            mMarginEndOfHint = dp2px(context, DEFAULT_MARGIN_END_OF_HINT_DP);
        }
        mPaintDivider.setColor(new Color(mDividerColor));
        mPaintDivider.setAntiAlias(true);
        mPaintDivider.setStyle(Paint.Style.STROKE_STYLE);
        mPaintDivider.setStrokeWidth(mDividerHeight);

        mPaintText.setColor(new Color(mTextColorNormal));
        mPaintText.setAntiAlias(true);
        mPaintText.setTextAlign(TextAlignment.CENTER);

        mPaintHint.setColor(new Color(mTextColorHint));
        mPaintHint.setAntiAlias(true);
        mPaintHint.setTextAlign(TextAlignment.CENTER);
        mPaintHint.setTextSize(mTextSizeHint);

        if (mShownCount % 2 == 0) {
            mShownCount++;
        }
        if (mMinShowIndex == -1 || mMaxShowIndex == -1) {
            updateValueForInit();
        }
        initHandler();
    }

    private void initHandler() {
        mHandlerThread = new HandlerThread("HandlerThread-For-Refreshing");
        mHandlerThread.start();

        mHandlerInNewThread = new EventHandler(mHandlerThread.getLooper()) {
            @Override
            public void processEvent(InnerEvent msg) {
                switch (msg.eventId) {
                    case HANDLER_WHAT_REFRESH:
                        if (!mScroller.isFinished()) {
                            if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
                                onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
                            }
                            mHandlerInNewThread.sendEvent(getMsg(HANDLER_WHAT_REFRESH, 0, 0, msg.object),
                                HANDLER_INTERVAL_REFRESH);
                        } else {
                            int duration = 0;
                            int willPickIndex;
                            // if scroller finished(not scrolling), then adjust the position
                            if (mCurrDrawFirstItemY != 0) { // need to adjust
                                if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
                                    onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
                                }
                                if (mCurrDrawFirstItemY < (-mItemHeight / 2)) {
                                    // adjust to scroll upward
                                    duration = (int) ((float) DEFAULT_INTERVAL_REVISE_DURATION
                                        * (mItemHeight + mCurrDrawFirstItemY) / mItemHeight);
                                    mScroller.startScroll(0, mCurrDrawGlobalY, 0,
                                        mItemHeight + mCurrDrawFirstItemY);
                                    willPickIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY
                                        + mItemHeight + mCurrDrawFirstItemY);
                                } else {
                                    // adjust to scroll downward
                                    duration = (int) ((float) DEFAULT_INTERVAL_REVISE_DURATION
                                        * (-mCurrDrawFirstItemY) / mItemHeight);
                                    mScroller.startScroll(0, mCurrDrawGlobalY, 0, mCurrDrawFirstItemY);
                                    willPickIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY + mCurrDrawFirstItemY);
                                }
                                postInvalidate();
                            } else {
                                onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                                // get the index which will be selected
                                willPickIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY);
                            }
                            InnerEvent changeMsg = getMsg(HANDLER_WHAT_LISTENER_VALUE_CHANGED,
                                mPrevPickedIndex, willPickIndex, msg.object);
                            if (mRespondChangeInMainThread) {
                                mHandlerInMainThread.sendEvent(changeMsg, duration * 2);
                            } else {
                                mHandlerInNewThread.sendEvent(changeMsg, duration * 2);
                            }
                        }
                        break;
                    case HANDLER_WHAT_LISTENER_VALUE_CHANGED:
                        respondPickedValueChanged(msg.getPacMap().getIntValue("arg1"),
                            msg.getPacMap().getIntValue("arg2"), msg.object);
                        break;
                    default:
                        break;
                }
            }
        };
        mHandlerInMainThread = new EventHandler(EventRunner.getMainEventRunner()) {
            @Override
            protected void processEvent(InnerEvent event) {
                super.processEvent(event);
                switch (event.eventId) {
                    case HANDLER_WHAT_REQUEST_LAYOUT:
                        postLayout();
                        break;
                    case HANDLER_WHAT_LISTENER_VALUE_CHANGED:
                        respondPickedValueChanged(event.getPacMap().getIntValue("arg1"),
                            event.getPacMap().getIntValue("arg2"), event.object);
                        break;
                    default:
                        break;
                }
            }
        };
    }

    public void postInvalidate() {
        mHandlerInMainThread.postTask(new Runnable() {
            @Override
            public void run() {
                invalidate();
            }
        });
    }

    private int mInScrollingPickedOldValue;
    private int mInScrollingPickedNewValue;

    private void respondPickedValueChangedInScrolling(int oldVal, int newVal) {
        mOnValueChangeListenerInScrolling.onValueChangeInScrolling(this, oldVal, newVal);
    }

    @Override
    public boolean onEstimateSize(int widthEstimateConfig, int heightEstimateConfig) {
        updateMaxWHOfDisplayedValues(false);
        int width = measureWidth(widthEstimateConfig);
        int height = measureHeight(heightEstimateConfig);
        setEstimatedSize(EstimateSpec.getChildSizeWithMode(width, width, EstimateSpec.PRECISE),
            EstimateSpec.getChildSizeWithMode(height, height, EstimateSpec.PRECISE));
        return true;
    }

    protected void onSizeChanged(int w, int h) {
        mViewWidth = w;
        mViewHeight = h;
        mItemHeight = mViewHeight / mShownCount;
        mViewCenterX = ((float) (mViewWidth + getPaddingLeft() - getPaddingRight())) / 2;
        int defaultValue = 0;
        if (getOneRecycleSize() > 1) {
            if (mHasInit) {
                defaultValue = getValue() - mMinValue;
            } else if (mCurrentItemIndexEffect) {
                defaultValue = mCurrDrawFirstItemIndex + (mShownCount - 1) / 2;
            } else {
                defaultValue = 0;
            }
        }
        correctPositionByDefaultValue(defaultValue, mWrapSelectorWheel && mWrapSelectorWheelCheck);
        updateFontAttr();
        updateNotWrapYLimit();
        updateDividerAttr();
        mHasInit = true;
    }

    protected void onAttachedToWindow() {
        if (mHandlerThread == null || !mHandlerThread.isAlive()) {
            initHandler();
        }
    }

    protected void onDetachedFromWindow() {
        mHandlerThread.quit();

        // These codes are for dialog or PopupWindow which will be used for more than once.
        // Not an elegant solution, if you have any good idea, please let me know, thank you.
        if (mItemHeight == 0) {
            return;
        }
        if (!mScroller.isFinished()) {
            mScroller.abortAnimation();
            mCurrDrawGlobalY = mScroller.getCurrValue(ScrollHelper.AXIS_Y);
            calculateFirstItemParameterByGlobalY();
            if (mCurrDrawFirstItemY != 0) {
                if (mCurrDrawFirstItemY < (-mItemHeight / 2)) {
                    mCurrDrawGlobalY = mCurrDrawGlobalY + mItemHeight + mCurrDrawFirstItemY;
                } else {
                    mCurrDrawGlobalY = mCurrDrawGlobalY + mCurrDrawFirstItemY;
                }
                calculateFirstItemParameterByGlobalY();
            }
            onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
        }

        // see the comments on mRespondChangeOnDetach, if mRespondChangeOnDetach is false,
        // please initialize NumberPickerView's data every time setting up NumberPickerView,
        // set the demo of GregorianLunarCalendar
        int currPickedIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY);
        if (currPickedIndex != mPrevPickedIndex && mRespondChangeOnDetach) {
            try {
                if (mOnValueChangeListener != null) {
                    mOnValueChangeListener.onValueChange(NumberPickerView.this,
                        mPrevPickedIndex + mMinValue, currPickedIndex + mMinValue);
                }
                if (mOnValueChangeListenerRaw != null) {
                    mOnValueChangeListenerRaw.onValueChangeRelativeToRaw(NumberPickerView.this,
                        mPrevPickedIndex, currPickedIndex, mDisplayedValues);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        mPrevPickedIndex = currPickedIndex;
    }

    public int getOneRecycleSize() {
        return mMaxShowIndex - mMinShowIndex + 1;
    }

    public int getRawContentSize() {
        if (mDisplayedValues != null) {
            return mDisplayedValues.length;
        }
        return 0;
    }

    public void setDisplayedValuesAndPickedIndex(String[] newDisplayedValues, int pickedIndex, boolean needRefresh) {
        stopScrolling();
        if (newDisplayedValues == null) {
            throw new IllegalArgumentException("newDisplayedValues should not be null.");
        }
        if (pickedIndex < 0) {
            throw new IllegalArgumentException("pickedIndex should not be negative, now pickedIndex is " + pickedIndex);
        }
        updateContent(newDisplayedValues);
        updateMaxWHOfDisplayedValues(true);
        updateNotWrapYLimit();
        updateValue();
        mPrevPickedIndex = pickedIndex + mMinShowIndex;
        correctPositionByDefaultValue(pickedIndex, mWrapSelectorWheel && mWrapSelectorWheelCheck);
        if (needRefresh) {
            mHandlerInNewThread.sendEvent(getMsg(HANDLER_WHAT_REFRESH), 0);
            postInvalidate();
        }
    }

    public void setDisplayedValues(String[] newDisplayedValues, boolean needRefresh) {
        setDisplayedValuesAndPickedIndex(newDisplayedValues, 0, needRefresh);
    }

    public void setDisplayedValues(String[] newDisplayedValues) {
        stopRefreshing();
        stopScrolling();
        if (newDisplayedValues == null) {
            throw new IllegalArgumentException("newDisplayedValues should not be null.");
        }

        if (mMaxValue - mMinValue + 1 > newDisplayedValues.length) {
            throw new IllegalArgumentException("mMaxValue - mMinValue + 1 should not be greater than "
                + "mDisplayedValues.length, now ((mMaxValue - mMinValue + 1) is " + (mMaxValue - mMinValue + 1)
                + " newDisplayedValues.length is " + newDisplayedValues.length
                + ", you need to set MaxValue and MinValue before setDisplayedValues(String[])");
        }
        updateContent(newDisplayedValues);
        updateMaxWHOfDisplayedValues(true);
        mPrevPickedIndex = 0 + mMinShowIndex;
        correctPositionByDefaultValue(0, mWrapSelectorWheel && mWrapSelectorWheelCheck);
        postInvalidate();
        mHandlerInMainThread.sendEvent(HANDLER_WHAT_REQUEST_LAYOUT);
    }

    /**
     * Gets the values to be displayed instead of string values.
     *
     * @return The displayed values.
     */
    public String[] getDisplayedValues() {
        return mDisplayedValues.clone();
    }

    public void setWrapSelectorWheel(boolean wrapSelectorWheel) {
        if (mWrapSelectorWheel != wrapSelectorWheel) {
            if (!wrapSelectorWheel) {
                if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
                    internalSetWrapToLinear();
                } else {
                    mPendingWrapToLinear = true;
                }
            } else {
                mWrapSelectorWheel = wrapSelectorWheel;
                updateWrapStateByContent();
                postInvalidate();
            }
        }
    }

    /**
     * get the "fromValue" by using getValue(), if your picker's minValue is not 0,
     * make sure you can get the accurate value by getValue(), or you can use
     * smoothScrollToValue(int fromValue, int toValue, boolean needRespond)
     *
     * @param toValue the value you want picker to scroll to
     */
    public void smoothScrollToValue(int toValue) {
        smoothScrollToValue(getValue(), toValue, true);
    }

    /**
     * get the "fromValue" by using getValue(), if your picker's minValue is not 0,
     * make sure you can get the accurate value by getValue(), or you can use
     * smoothScrollToValue(int fromValue, int toValue, boolean needRespond)
     *
     * @param toValue the value you want picker to scroll to
     * @param needRespond set if you want picker to respond onValueChange listener
     */
    public void smoothScrollToValue(int toValue, boolean needRespond) {
        smoothScrollToValue(getValue(), toValue, needRespond);
    }

    public void smoothScrollToValue(int fromValue, int toValue) {
        smoothScrollToValue(fromValue, toValue, true);
    }

    /**
     * smoothScrollToValue
     *
     * @param fromValue need to set the fromValue, can be greater than mMaxValue or less than mMinValue
     * @param toValue the value you want picker to scroll to
     * @param needRespond need Respond to the ValueChange callback When Scrolling, default is false
     */
    public void smoothScrollToValue(int fromValue, int toValue, boolean needRespond) {
        int deltaIndex;
        fromValue = refineValueByLimit(fromValue, mMinValue, mMaxValue,
            mWrapSelectorWheel && mWrapSelectorWheelCheck);
        toValue = refineValueByLimit(toValue, mMinValue, mMaxValue,
            mWrapSelectorWheel && mWrapSelectorWheelCheck);
        if (mWrapSelectorWheel && mWrapSelectorWheelCheck) {
            deltaIndex = toValue - fromValue;
            int halfOneRecycleSize = getOneRecycleSize() / 2;
            if (deltaIndex < -halfOneRecycleSize || halfOneRecycleSize < deltaIndex) {
                deltaIndex = deltaIndex > 0 ? deltaIndex - getOneRecycleSize() : deltaIndex + getOneRecycleSize();
            }
        } else {
            deltaIndex = toValue - fromValue;
        }
        setValue(fromValue);
        if (fromValue == toValue) {
            return;
        }
        scrollByIndexSmoothly(deltaIndex, needRespond);
    }

    /**
     * simplify the "setDisplayedValue() + setMinValue() + setMaxValue()" process,
     * default minValue is 0, and make sure you do NOT change the minValue.
     *
     * @param display new values to be displayed
     */
    public void refreshByNewDisplayedValues(String[] display) {
        int minValue = getMinValue();

        int oldMaxValue = getMaxValue();
        int oldSpan = oldMaxValue - minValue + 1;

        int newMaxValue = display.length - 1;
        int newSpan = newMaxValue - minValue + 1;

        if (newSpan > oldSpan) {
            setDisplayedValues(display);
            setMaxValue(newMaxValue);
        } else {
            setMaxValue(newMaxValue);
            setDisplayedValues(display);
        }
    }

    /**
     * used by handlers to respond onchange callbacks
     *
     * @param oldVal prevPicked value
     * @param newVal currPicked value
     * @param respondChange if want to respond onchange callbacks
     */
    private void respondPickedValueChanged(int oldVal, int newVal, Object respondChange) {
        onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
        if (oldVal != newVal) {
            if (respondChange == null || !(respondChange instanceof Boolean) || (Boolean) respondChange) {
                if (mOnValueChangeListener != null) {
                    mOnValueChangeListener.onValueChange(NumberPickerView.this, oldVal + mMinValue, newVal + mMinValue);
                }
                if (mOnValueChangeListenerRaw != null) {
                    mOnValueChangeListenerRaw.onValueChangeRelativeToRaw(NumberPickerView.this,
                        oldVal, newVal, mDisplayedValues);
                }
            }
        }
        mPrevPickedIndex = newVal;
        if (mPendingWrapToLinear) {
            mPendingWrapToLinear = false;
            internalSetWrapToLinear();
        }
    }

    private void scrollByIndexSmoothly(int deltaIndex) {
        scrollByIndexSmoothly(deltaIndex, true);
    }

    /**
     * scrollByIndexSmoothly
     *
     * @param deltaIndex the delta index it will scroll by
     * @param needRespond need Respond to the ValueChange callback When Scrolling, default is false
     */
    private void scrollByIndexSmoothly(int deltaIndex, boolean needRespond) {
        if (!(mWrapSelectorWheel && mWrapSelectorWheelCheck)) {
            int willPickRawIndex = getPickedIndexRelativeToRaw();
            if (willPickRawIndex + deltaIndex > mMaxShowIndex) {
                deltaIndex = mMaxShowIndex - willPickRawIndex;
            } else if (willPickRawIndex + deltaIndex < mMinShowIndex) {
                deltaIndex = mMinShowIndex - willPickRawIndex;
            }
        }
        int duration;
        int dy;
        if (mCurrDrawFirstItemY < (-mItemHeight / 2)) {
            // scroll upwards for a distance of less than mItemHeight
            dy = mItemHeight + mCurrDrawFirstItemY;
            duration = (int) ((float) DEFAULT_INTERVAL_REVISE_DURATION
                * (mItemHeight + mCurrDrawFirstItemY) / mItemHeight);
            if (deltaIndex < 0) {
                duration = -duration - deltaIndex * DEFAULT_INTERVAL_REVISE_DURATION;
            } else {
                duration = duration + deltaIndex * DEFAULT_INTERVAL_REVISE_DURATION;
            }
        } else {
            // scroll downwards for a distance of less than mItemHeight
            dy = mCurrDrawFirstItemY;
            duration = (int) ((float) DEFAULT_INTERVAL_REVISE_DURATION * (-mCurrDrawFirstItemY) / mItemHeight);
            if (deltaIndex < 0) {
                duration = duration - deltaIndex * DEFAULT_INTERVAL_REVISE_DURATION;
            } else {
                duration = duration + deltaIndex * DEFAULT_INTERVAL_REVISE_DURATION;
            }
        }
        dy = dy + deltaIndex * mItemHeight;
        if (duration < DEFAULT_MIN_SCROLL_BY_INDEX_DURATION) {
            duration = DEFAULT_MIN_SCROLL_BY_INDEX_DURATION;
        }
        if (duration > DEFAULT_MAX_SCROLL_BY_INDEX_DURATION) {
            duration = DEFAULT_MAX_SCROLL_BY_INDEX_DURATION;
        }
        mScroller.startScroll(0, mCurrDrawGlobalY, 0, dy);
        if (needRespond) {
            mHandlerInNewThread.sendEvent(getMsg(HANDLER_WHAT_REFRESH), duration / 4);
        } else {
            mHandlerInNewThread.sendEvent(getMsg(HANDLER_WHAT_REFRESH, 0, 0,
                Boolean.valueOf(needRespond)), duration / 4);
        }
        postInvalidate();
    }

    public int getMinValue() {
        return mMinValue;
    }

    public int getMaxValue() {
        return mMaxValue;
    }

    public void setMinValue(int minValue) {
        mMinValue = minValue;
        mMinShowIndex = 0;
        updateNotWrapYLimit();
    }

    // compatible for android.widget.NumberPicker
    public void setMaxValue(int maxValue) {
        if (mDisplayedValues == null) {
            throw new NullPointerException("mDisplayedValues should not be null");
        }
        if (maxValue - mMinValue + 1 > mDisplayedValues.length) {
            throw new IllegalArgumentException("(maxValue - mMinValue + 1) should not be greater than "
                + "mDisplayedValues.length now (maxValue - mMinValue + 1) is " + (maxValue - mMinValue + 1)
                + " and mDisplayedValues.length is " + mDisplayedValues.length);
        }
        mMaxValue = maxValue;
        mMaxShowIndex = mMaxValue - mMinValue + mMinShowIndex;
        setMinAndMaxShowIndex(mMinShowIndex, mMaxShowIndex);
        updateNotWrapYLimit();
    }

    // compatible for android.widget.NumberPicker
    public void setValue(int value) {
        if (value < mMinValue) {
            throw new IllegalArgumentException("should not set a value less than mMinValue, value is " + value);
        }
        if (value > mMaxValue) {
            throw new IllegalArgumentException("should not set a value greater than mMaxValue, value is " + value);
        }
        setPickedIndexRelativeToRaw(value - mMinValue);
    }

    // compatible for android.widget.NumberPicker
    public int getValue() {
        return getPickedIndexRelativeToRaw() + mMinValue;
    }

    public String getContentByCurrValue() {
        return mDisplayedValues[getValue() - mMinValue];
    }

    public boolean getWrapSelectorWheel() {
        return mWrapSelectorWheel;
    }

    public boolean getWrapSelectorWheelAbsolutely() {
        return mWrapSelectorWheel && mWrapSelectorWheelCheck;
    }

    public void setHintText(String hintText) {
        if (isStringEqual(mHintText, hintText)) {
            return;
        }
        mHintText = hintText;
        mTextSizeHintCenterYOffset = getTextCenterYOffset(mPaintHint.getFontMetrics());
        mWidthOfHintText = getTextWidth(mHintText, mPaintHint);
        mHandlerInMainThread.sendEvent(HANDLER_WHAT_REQUEST_LAYOUT);
    }

    public void setPickedIndexRelativeToMin(int pickedIndexToMin) {
        if (0 <= pickedIndexToMin && pickedIndexToMin < getOneRecycleSize()) {
            mPrevPickedIndex = pickedIndexToMin + mMinShowIndex;
            correctPositionByDefaultValue(pickedIndexToMin, mWrapSelectorWheel && mWrapSelectorWheelCheck);
            postInvalidate();
        }
    }

    public void setNormalTextColor(int normalTextColor) {
        if (mTextColorNormal == normalTextColor) {
            return;
        }
        mTextColorNormal = normalTextColor;
        postInvalidate();
    }

    public void setSelectedTextColor(int selectedTextColor) {
        if (mTextColorSelected == selectedTextColor) {
            return;
        }
        mTextColorSelected = selectedTextColor;
        postInvalidate();
    }

    public void setHintTextColor(int hintTextColor) {
        if (mTextColorHint == hintTextColor) {
            return;
        }
        mTextColorHint = hintTextColor;
        mPaintHint.setColor(new Color(mTextColorHint));
        postInvalidate();
    }

    public void setDividerColor(int dividerColor) {
        if (mDividerColor == dividerColor) {
            return;
        }
        mDividerColor = dividerColor;
        mPaintDivider.setColor(new Color(mDividerColor));
        postInvalidate();
    }

    public void setPickedIndexRelativeToRaw(int pickedIndexToRaw) {
        if (mMinShowIndex > -1) {
            if (mMinShowIndex <= pickedIndexToRaw && pickedIndexToRaw <= mMaxShowIndex) {
                mPrevPickedIndex = pickedIndexToRaw;
                correctPositionByDefaultValue(pickedIndexToRaw - mMinShowIndex,
                    mWrapSelectorWheel && mWrapSelectorWheelCheck);
                postInvalidate();
            }
        }
    }

    public int getPickedIndexRelativeToRaw() {
        int willPickIndex;
        if (mCurrDrawFirstItemY != 0) {
            if (mCurrDrawFirstItemY < (-mItemHeight / 2)) {
                willPickIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY + mItemHeight + mCurrDrawFirstItemY);
            } else {
                willPickIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY + mCurrDrawFirstItemY);
            }
        } else {
            willPickIndex = getWillPickIndexByGlobalY(mCurrDrawGlobalY);
        }
        return willPickIndex;
    }

    public void setMinAndMaxShowIndex(int minShowIndex, int maxShowIndex) {
        setMinAndMaxShowIndex(minShowIndex, maxShowIndex, true);
    }

    public void setMinAndMaxShowIndex(int minShowIndex, int maxShowIndex, boolean needRefresh) {
        if (minShowIndex > maxShowIndex) {
            throw new IllegalArgumentException("minShowIndex should be less than maxShowIndex, minShowIndex is "
                + minShowIndex + ", maxShowIndex is " + maxShowIndex + ".");
        }
        if (mDisplayedValues == null) {
            throw new IllegalArgumentException("mDisplayedValues should not be null,"
                + " you need to set mDisplayedValues first.");
        } else {
            if (minShowIndex < 0) {
                throw new IllegalArgumentException("minShowIndex should not be less than 0, now minShowIndex is "
                    + minShowIndex);
            } else if (minShowIndex > mDisplayedValues.length - 1) {
                throw new IllegalArgumentException("minShowIndex should not be greater than "
                    + "(mDisplayedValues.length - 1), now (mDisplayedValues.length - 1) is "
                    + (mDisplayedValues.length - 1) + " minShowIndex is " + minShowIndex);
            }

            if (maxShowIndex < 0) {
                throw new IllegalArgumentException("maxShowIndex should not be less than 0, now maxShowIndex is "
                    + maxShowIndex);
            } else if (maxShowIndex > mDisplayedValues.length - 1) {
                throw new IllegalArgumentException("maxShowIndex should not be greater than "
                    + "(mDisplayedValues.length - 1), now (mDisplayedValues.length - 1) is "
                    + (mDisplayedValues.length - 1) + " maxShowIndex is " + maxShowIndex);
            }
        }
        mMinShowIndex = minShowIndex;
        mMaxShowIndex = maxShowIndex;
        if (needRefresh) {
            mPrevPickedIndex = 0 + mMinShowIndex;
            correctPositionByDefaultValue(0, mWrapSelectorWheel && mWrapSelectorWheelCheck);
            postInvalidate();
        }
    }

    /**
     * set the friction of scroller, it will effect the scroller's acceleration when fling
     *
     * @param friction default is ViewConfiguration.get(mContext).getScrollFriction()
     * if setFriction(2 * ViewConfiguration.get(mContext).getScrollFriction()),
     * the friction will be twice as much as before
     * @throws IllegalArgumentException
     */
    public void setFriction(float friction) {
        if (friction <= 0) {
            throw new IllegalArgumentException("you should set a a positive float friction, now friction is "
                + friction);
        }
        mFriction = 0.015f / friction;
    }

    //compatible for NumberPicker
    private void onScrollStateChange(int scrollState) {
        if (mScrollState == scrollState) {
            return;
        }
        mScrollState = scrollState;
        if (mOnScrollListener != null) {
            mOnScrollListener.onScrollStateChange(this, scrollState);
        }
    }

    // compatible for NumberPicker
    public void setOnScrollListener(OnScrollListener listener) {
        mOnScrollListener = listener;
    }

    // compatible for NumberPicker
    public void setOnValueChangedListener(OnValueChangeListener listener) {
        mOnValueChangeListener = listener;
    }

    public void setOnValueChangedListenerRelativeToRaw(OnValueChangeListenerRelativeToRaw listener) {
        mOnValueChangeListenerRaw = listener;
    }

    public void setOnValueChangeListenerInScrolling(OnValueChangeListenerInScrolling listener) {
        mOnValueChangeListenerInScrolling = listener;
    }

    public void setContentTextTypeface(Font font) {
        mPaintText.setFont(font);
    }

    public void setHintTextTypeface(Font font) {
        mPaintHint.setFont(font);
    }

    // return index relative to mDisplayedValues from 0.
    private int getWillPickIndexByGlobalY(int globalY) {
        if (mItemHeight == 0) {
            return 0;
        }
        int willPickIndex = globalY / mItemHeight + mShownCount / 2;
        int index = getIndexByRawIndex(willPickIndex, getOneRecycleSize(),
            mWrapSelectorWheel && mWrapSelectorWheelCheck);
        if (0 <= index && index < getOneRecycleSize()) {
            return index + mMinShowIndex;
        }
        int num = index % (mMaxValue + 1);
        if (num >= 0) {
            return num;
        } else {
            return mMaxValue + 1 - Math.abs(num);
        }
    }

    private int getIndexByRawIndex(int index, int size, boolean wrap) {
        if (size <= 0) {
            return 0;
        }
        if (wrap) {
            index = index % size;
            if (index < 0) {
                index = index + size;
            }
            return index;
        } else {
            return index;
        }
    }

    private void internalSetWrapToLinear() {
        int rawIndex = getPickedIndexRelativeToRaw();
        correctPositionByDefaultValue(rawIndex - mMinShowIndex, false);
        mWrapSelectorWheel = false;
        postInvalidate();
    }

    private void updateDividerAttr() {
        mDividerIndex0 = mShownCount / 2;
        mDividerIndex1 = mDividerIndex0 + 1;
        dividerY0 = mDividerIndex0 * mViewHeight / (float)mShownCount;
        dividerY1 = mDividerIndex1 * mViewHeight / (float)mShownCount;
        if (mDividerMarginL < 0) {
            mDividerMarginL = 0;
        }
        if (mDividerMarginR < 0) {
            mDividerMarginR = 0;
        }

        if (mDividerMarginL + mDividerMarginR == 0) {
            return;
        }
        if (getPaddingLeft() + mDividerMarginL >= mViewWidth - getPaddingRight() - mDividerMarginR) {
            int surplusMargin = getPaddingLeft() + mDividerMarginL + getPaddingRight() + mDividerMarginR - mViewWidth;
            mDividerMarginL = (int) (mDividerMarginL - (float) surplusMargin
                * mDividerMarginL / (mDividerMarginL + mDividerMarginR));
            mDividerMarginR = (int) (mDividerMarginR - (float) surplusMargin
                * mDividerMarginR / (mDividerMarginL + mDividerMarginR));
        }
    }

    private int mNotWrapLimitYTop;
    private int mNotWrapLimitYBottom;

    private void updateFontAttr() {
        if (mTextSizeNormal > mItemHeight) {
            mTextSizeNormal = mItemHeight;
        }
        if (mTextSizeSelected > mItemHeight) {
            mTextSizeSelected = mItemHeight;
        }

        if (mPaintHint == null) {
            throw new IllegalArgumentException("mPaintHint should not be null.");
        }
        mPaintHint.setTextSize(mTextSizeHint);
        mTextSizeHintCenterYOffset = getTextCenterYOffset(mPaintHint.getFontMetrics());
        mWidthOfHintText = getTextWidth(mHintText, mPaintHint);

        if (mPaintText == null) {
            throw new IllegalArgumentException("mPaintText should not be null.");
        }
        mPaintText.setTextSize(mTextSizeSelected);
        mTextSizeSelectedCenterYOffset = getTextCenterYOffset(mPaintText.getFontMetrics());
        mPaintText.setTextSize(mTextSizeNormal);
        mTextSizeNormalCenterYOffset = getTextCenterYOffset(mPaintText.getFontMetrics());
    }

    private void updateNotWrapYLimit() {
        mNotWrapLimitYTop = 0;
        mNotWrapLimitYBottom = -mShownCount * mItemHeight;
        if (mDisplayedValues != null) {
            mNotWrapLimitYTop = (getOneRecycleSize() - mShownCount / 2 - 1) * mItemHeight;
            mNotWrapLimitYBottom = -(mShownCount / 2) * mItemHeight;
        }
    }

    private float downYGlobal = 0;
    private float downY = 0;
    private float currY = 0;

    private int limitY(int currDrawGlobalYPreferred) {
        if (mWrapSelectorWheel && mWrapSelectorWheelCheck) {
            return currDrawGlobalYPreferred;
        }
        if (currDrawGlobalYPreferred < mNotWrapLimitYBottom) {
            currDrawGlobalYPreferred = mNotWrapLimitYBottom;
        } else if (currDrawGlobalYPreferred > mNotWrapLimitYTop) {
            currDrawGlobalYPreferred = mNotWrapLimitYTop;
        }
        return currDrawGlobalYPreferred;
    }

    private boolean mFlagMayPress = false;

    @Override
    public boolean onTouchEvent(Component component, TouchEvent event) {
        if (!isEnabled()) {
            return false;
        }
        if (mItemHeight == 0) {
            return true;
        }
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityDetector.obtainInstance();
        }
        mVelocityTracker.addEvent(event);
        currY = event.getPointerPosition(0).getY();

        switch (event.getAction()) {
            case TouchEvent.PRIMARY_POINT_DOWN:
                mFlagMayPress = true;
                mHandlerInNewThread.removeEvent(HANDLER_WHAT_REFRESH);
                stopScrolling();
                downY = currY;
                downYGlobal = mCurrDrawGlobalY;
                onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                break;
            case TouchEvent.POINT_MOVE:
                float spanY = downY - currY;
                if (mFlagMayPress && (-mScaledTouchSlop < spanY && spanY < mScaledTouchSlop)) {
                } else {
                    mFlagMayPress = false;
                    mCurrDrawGlobalY = limitY((int) (downYGlobal + spanY));
                    calculateFirstItemParameterByGlobalY();
                    invalidate();
                }
                onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
                break;
            case TouchEvent.PRIMARY_POINT_UP:
                if (mFlagMayPress) {
                    click(event);
                } else {
                    final VelocityDetector velocityTracker = mVelocityTracker;
                    velocityTracker.calculateCurrentVelocity(1000);
                    int velocityY = (int) (velocityTracker.getVerticalVelocity() * mFriction);
                    if (Math.abs(velocityY) > mMiniVelocityFling) {
                        mScroller.doFling(0, mCurrDrawGlobalY, 0, -velocityY,
                            Integer.MIN_VALUE, Integer.MAX_VALUE, limitY(Integer.MIN_VALUE), limitY(Integer.MAX_VALUE));
                        invalidate();
                        onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
                    }
                    mHandlerInNewThread.sendEvent(getMsg(HANDLER_WHAT_REFRESH), 0);
                    releaseVelocityTracker();
                }
                break;
            case TouchEvent.CANCEL:
                downYGlobal = mCurrDrawGlobalY;
                stopScrolling();
                mHandlerInNewThread.sendEvent(getMsg(HANDLER_WHAT_REFRESH), 0);
                break;
            default:
                break;
        }
        return true;
    }

    private void click(TouchEvent event) {
        float y = event.getPointerPosition(0).getY();
        for (int i = 0; i < mShownCount; i++) {
            if (mItemHeight * i <= y && y < mItemHeight * (i + 1)) {
                clickItem(i);
                break;
            }
        }
    }

    private void clickItem(int showCountIndex) {
        if (0 <= showCountIndex && showCountIndex < mShownCount) {
            // clicked the showCountIndex of the view
            scrollByIndexSmoothly(showCountIndex - mShownCount / 2);
        } else {
            // wrong
        }
    }

    private float getTextCenterYOffset(Paint.FontMetrics fontMetrics) {
        if (fontMetrics == null) {
            return 0;
        }
        return Math.abs(fontMetrics.top + fontMetrics.bottom) / 2;
    }

    private int mViewWidth;
    private int mViewHeight;
    private int mItemHeight;
    private float dividerY0;
    private float dividerY1;
    private float mViewCenterX;

    // defaultPickedIndex relative to the shown part
    private void correctPositionByDefaultValue(int defaultPickedIndex, boolean wrap) {
        mCurrDrawFirstItemIndex = defaultPickedIndex - (mShownCount - 1) / 2;
        mCurrDrawFirstItemIndex = getIndexByRawIndex(mCurrDrawFirstItemIndex, getOneRecycleSize(), wrap);
        if (mItemHeight == 0) {
            mCurrentItemIndexEffect = true;
        } else {
            mCurrDrawGlobalY = mCurrDrawFirstItemIndex * mItemHeight;

            mInScrollingPickedOldValue = mCurrDrawFirstItemIndex + mShownCount / 2;
            mInScrollingPickedOldValue = mInScrollingPickedOldValue % getOneRecycleSize();
            if (mInScrollingPickedOldValue < 0) {
                mInScrollingPickedOldValue = mInScrollingPickedOldValue + getOneRecycleSize();
            }
            mInScrollingPickedNewValue = mInScrollingPickedOldValue;
            calculateFirstItemParameterByGlobalY();
        }
    }

    // first shown item's content index, corresponding to the Index of mDisplayedValued
    private int mCurrDrawFirstItemIndex = 0;
    // the first shown item's Y
    private int mCurrDrawFirstItemY = 0;
    // global Y corresponding to scroller
    private int mCurrDrawGlobalY = 0;

    public void computeScroll() {
        if (mItemHeight == 0) {
            return;
        }
        if (!mScroller.isFinished()) {
            mScroller.updateScroll();
            mCurrDrawGlobalY = mScroller.getCurrValue(ScrollHelper.AXIS_Y);
            calculateFirstItemParameterByGlobalY();
            postInvalidate();
        }
    }

    private void calculateFirstItemParameterByGlobalY() {
        mCurrDrawFirstItemIndex = (int) Math.floor((float) mCurrDrawGlobalY / mItemHeight);
        mCurrDrawFirstItemY = -(mCurrDrawGlobalY - mCurrDrawFirstItemIndex * mItemHeight);
        if (mOnValueChangeListenerInScrolling != null) {
            if (-mCurrDrawFirstItemY > mItemHeight / 2) {
                mInScrollingPickedNewValue = mCurrDrawFirstItemIndex + 1 + mShownCount / 2;
            } else {
                mInScrollingPickedNewValue = mCurrDrawFirstItemIndex + mShownCount / 2;
            }
            mInScrollingPickedNewValue = mInScrollingPickedNewValue % getOneRecycleSize();
            if (mInScrollingPickedNewValue < 0) {
                mInScrollingPickedNewValue = mInScrollingPickedNewValue + getOneRecycleSize();
            }
            if (mInScrollingPickedOldValue != mInScrollingPickedNewValue) {
                respondPickedValueChangedInScrolling(mInScrollingPickedOldValue + mMinValue,
                    mInScrollingPickedNewValue + mMinValue);
            }
            mInScrollingPickedOldValue = mInScrollingPickedNewValue;
        }
    }

    private void releaseVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.clear();
            mVelocityTracker = null;
        }
    }

    private void updateMaxWHOfDisplayedValues(boolean needRequestLayout) {
        updateMaxWidthOfDisplayedValues();
        updateMaxHeightOfDisplayedValues();
        if (needRequestLayout
            && (mSpecModeW == EstimateSpec.NOT_EXCEED || mSpecModeH == EstimateSpec.NOT_EXCEED)) {
            mHandlerInMainThread.sendEvent(HANDLER_WHAT_REQUEST_LAYOUT);
        }
    }

    private int mSpecModeW = EstimateSpec.UNCONSTRAINT;
    private int mSpecModeH = EstimateSpec.UNCONSTRAINT;

    private int measureWidth(int measureSpec) {
        int result;
        int specMode = mSpecModeW = EstimateSpec.getMode(measureSpec);
        int specSize = EstimateSpec.getSize(measureSpec);

        if (specMode == EstimateSpec.PRECISE) {
            result = specSize;
        } else {
            int marginOfHint = Math.max(mWidthOfHintText, mWidthOfAlterHint) == 0 ? 0 : mMarginEndOfHint;
            int gapOfHint = Math.max(mWidthOfHintText, mWidthOfAlterHint) == 0 ? 0 : mMarginStartOfHint;

            int maxWidth = Math.max(mMaxWidthOfAlterArrayWithMeasureHint,
                Math.max(mMaxWidthOfDisplayedValues, mMaxWidthOfAlterArrayWithoutMeasureHint)
                    + 2 * (gapOfHint + Math.max(mWidthOfHintText, mWidthOfAlterHint)
                    + marginOfHint + 2 * mItemPaddingHorizontal));
            result = this.getPaddingLeft() + this.getPaddingRight() + maxWidth; // MeasureSpec.UNSPECIFIED
            if (specMode == EstimateSpec.NOT_EXCEED) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    private int measureHeight(int measureSpec) {
        int result;
        int specMode = mSpecModeH = EstimateSpec.getMode(measureSpec);
        int specSize = EstimateSpec.getSize(measureSpec);

        if (specMode == EstimateSpec.PRECISE) {
            result = specSize;
        } else {
            int maxHeight = mShownCount * (mMaxHeightOfDisplayedValues + 2 * mItemPaddingVertical);
            result = this.getPaddingTop() + this.getPaddingBottom() + maxHeight; // MeasureSpec.UNSPECIFIED
            if (specMode == EstimateSpec.NOT_EXCEED) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        drawSelectedItemBackground(canvas);
        drawContent(canvas);
        drawLine(canvas);
        drawHint(canvas);
        mHandlerInMainThread.postTask(new Runnable() {
            @Override
            public void run() {
                computeScroll();
            }
        });
    }

    private void drawSelectedItemBackground(Canvas canvas) {
        if (mSelectedItemBackground != null) {
            canvas.save();
            int left = getPaddingLeft() + mDividerMarginL;
            int top = (int) dividerY0;
            int right = mViewWidth - getPaddingRight() - mDividerMarginR;
            int bottom = (int) dividerY1;
            mSelectedItemBackground.setBounds(left, top, right, bottom);
            mSelectedItemBackground.drawToCanvas(canvas);
            canvas.restore();
        }
    }

    private void drawContent(Canvas canvas) {
        int index;
        int textColor;
        float textSize;
        float fraction = 0f; // fraction of the item in state between normal and selected, in[0, 1]
        float textSizeCenterYOffset;

        for (int i = 0; i < mShownCount + 1; i++) {
            float y = mCurrDrawFirstItemY + mItemHeight * i;
            index = getIndexByRawIndex(mCurrDrawFirstItemIndex + i, getOneRecycleSize(),
                mWrapSelectorWheel && mWrapSelectorWheelCheck);
            if (i == mShownCount / 2) { // this will be picked
                fraction = (float) (mItemHeight + mCurrDrawFirstItemY) / mItemHeight;
                textColor = getEvaluateColor(fraction, mTextColorNormal, mTextColorSelected);
                textSize = getEvaluateSize(fraction, mTextSizeNormal, mTextSizeSelected);
                textSizeCenterYOffset = getEvaluateSize(fraction, mTextSizeNormalCenterYOffset,
                    mTextSizeSelectedCenterYOffset);
            } else if (i == mShownCount / 2 + 1) {
                textColor = getEvaluateColor(1 - fraction, mTextColorNormal, mTextColorSelected);
                textSize = getEvaluateSize(1 - fraction, mTextSizeNormal, mTextSizeSelected);
                textSizeCenterYOffset = getEvaluateSize(1 - fraction, mTextSizeNormalCenterYOffset,
                    mTextSizeSelectedCenterYOffset);
            } else {
                textColor = mTextColorNormal;
                textSize = mTextSizeNormal;
                textSizeCenterYOffset = mTextSizeNormalCenterYOffset;
            }
            mPaintText.setColor(new Color(textColor));
            mPaintText.setTextSize((int) textSize);

            if (0 <= index && index < getOneRecycleSize()) {
                CharSequence str = mDisplayedValues[index + mMinShowIndex];
                if (mTextEllipsize != null) {
                    //todo str = TextUtils.ellipsize(str, mPaintText, getWidth() - 2 * mItemPaddingHorizontal, getEllipsizeType());
                }
                canvas.drawText(mPaintText, str.toString(), mViewCenterX,
                    y + mItemHeight / 2f + textSizeCenterYOffset);
            } else if (!TextTool.isNullOrEmpty(mEmptyItemHint)) {
                canvas.drawText(mPaintText, mEmptyItemHint, mViewCenterX,
                    y + mItemHeight / 2f + textSizeCenterYOffset);
            }
        }
    }

    private Text.TruncationMode getEllipsizeType() {
        switch (mTextEllipsize) {
            case TEXT_ELLIPSIZE_START:
                return Text.TruncationMode.ELLIPSIS_AT_START;
            case TEXT_ELLIPSIZE_MIDDLE:
                return Text.TruncationMode.ELLIPSIS_AT_MIDDLE;
            case TEXT_ELLIPSIZE_END:
                return Text.TruncationMode.ELLIPSIS_AT_END;
            default:
                throw new IllegalArgumentException("Illegal text ellipsize type.");
        }
    }

    private void drawLine(Canvas canvas) {
        if (mShowDivider) {
            canvas.drawLine(getPaddingLeft() + mDividerMarginL,
                dividerY0, mViewWidth - getPaddingRight() - mDividerMarginR, dividerY0, mPaintDivider);
            canvas.drawLine(getPaddingLeft() + mDividerMarginL,
                dividerY1, mViewWidth - getPaddingRight() - mDividerMarginR, dividerY1, mPaintDivider);
        }
    }

    private void drawHint(Canvas canvas) {
        if (TextTool.isNullOrEmpty(mHintText)) {
            return;
        }
        canvas.drawText(mPaintHint, mHintText,
            mViewCenterX + (mMaxWidthOfDisplayedValues + mWidthOfHintText) / 2f + mMarginStartOfHint,
            (dividerY0 + dividerY1) / 2 + mTextSizeHintCenterYOffset);
    }

    private void updateMaxWidthOfDisplayedValues() {
        int savedTextSize = mPaintText.getTextSize();
        mPaintText.setTextSize(mTextSizeSelected);
        mMaxWidthOfDisplayedValues = getMaxWidthOfTextArray(mDisplayedValues, mPaintText);
        mMaxWidthOfAlterArrayWithMeasureHint = getMaxWidthOfTextArray(mAlterTextArrayWithMeasureHint, mPaintText);
        mMaxWidthOfAlterArrayWithoutMeasureHint = getMaxWidthOfTextArray(mAlterTextArrayWithoutMeasureHint, mPaintText);
        mPaintText.setTextSize(mTextSizeHint);
        mWidthOfAlterHint = getTextWidth(mAlterHint, mPaintText);
        mPaintText.setTextSize(savedTextSize);
    }

    private int getMaxWidthOfTextArray(CharSequence[] array, Paint paint) {
        if (array == null) {
            return 0;
        }
        int maxWidth = 0;
        for (CharSequence item : array) {
            if (item != null) {
                int itemWidth = getTextWidth(item, paint);
                maxWidth = Math.max(itemWidth, maxWidth);
            }
        }
        return maxWidth;
    }

    private int getTextWidth(CharSequence text, Paint paint) {
        if (TextTool.isNullOrEmpty(text)) {
            return 0;
        }
        String key = text.toString();

        if (mTextWidthCache.containsKey(key)) {
            Integer integer = mTextWidthCache.get(key);
            if (integer != null) {
                return integer;
            }
        }

        int value = (int) (paint.measureText(key) + 0.5f);
        mTextWidthCache.put(key, value);
        return value;
    }

    private void updateMaxHeightOfDisplayedValues() {
        int savedTextSize = mPaintText.getTextSize();
        mPaintText.setTextSize(mTextSizeSelected);
        mMaxHeightOfDisplayedValues = (int) (mPaintText.getFontMetrics().bottom
            - mPaintText.getFontMetrics().top + 0.5);
        mPaintText.setTextSize(savedTextSize);
    }

    private void updateContentAndIndex(String[] newDisplayedValues) {
        mMinShowIndex = 0;
        mMaxShowIndex = newDisplayedValues.length - 1;
        mDisplayedValues = newDisplayedValues;
        updateWrapStateByContent();
    }

    private void updateContent(String[] newDisplayedValues) {
        mDisplayedValues = newDisplayedValues;
        updateWrapStateByContent();
    }

    // used in setDisplayedValues
    private void updateValue() {
        inflateDisplayedValuesIfNull();
        updateWrapStateByContent();
        mMinShowIndex = 0;
        mMaxShowIndex = mDisplayedValues.length - 1;
    }

    private void updateValueForInit() {
        inflateDisplayedValuesIfNull();
        updateWrapStateByContent();
        if (mMinShowIndex == -1) {
            mMinShowIndex = 0;
        }
        if (mMaxShowIndex == -1) {
            mMaxShowIndex = mDisplayedValues.length - 1;
        }
        setMinAndMaxShowIndex(mMinShowIndex, mMaxShowIndex, false);
    }

    private void inflateDisplayedValuesIfNull() {
        if (mDisplayedValues == null) {
            mDisplayedValues = new String[1];
            mDisplayedValues[0] = "0";
        }
    }

    private void updateWrapStateByContent() {
        mWrapSelectorWheelCheck = mDisplayedValues.length <= mShownCount ? false : true;
    }

    private int refineValueByLimit(int value, int minValue, int maxValue, boolean wrap) {
        if (wrap) {
            if (value > maxValue) {
                value = (value - maxValue) % getOneRecycleSize() + minValue - 1;
            } else if (value < minValue) {
                value = (value - minValue) % getOneRecycleSize() + maxValue + 1;
            }
            return value;
        } else {
            if (value > maxValue) {
                value = maxValue;
            } else if (value < minValue) {
                value = minValue;
            }
            return value;
        }
    }

    private void stopRefreshing() {
        if (mHandlerInNewThread != null) {
            mHandlerInNewThread.removeEvent(HANDLER_WHAT_REFRESH);
        }
    }

    public void stopScrolling() {
        if (mScroller != null) {
            if (!mScroller.isFinished()) {
                mScroller.startScroll(0, mScroller.getCurrValue(ScrollHelper.AXIS_Y), 0, 0);
                mScroller.abortAnimation();
                postInvalidate();
            }
        }
    }

    public void stopScrollingAndCorrectPosition() {
        stopScrolling();
        if (mHandlerInNewThread != null) {
            mHandlerInNewThread.sendEvent(getMsg(HANDLER_WHAT_REFRESH), 0);
        }
    }

    private InnerEvent getMsg(int what) {
        return getMsg(what, 0, 0, null);
    }

    private InnerEvent getMsg(int what, int arg1, int arg2, Object obj) {
        InnerEvent msg = InnerEvent.get();
        msg.eventId = what;
        PacMap arg = new PacMap();
        arg.putIntValue("arg1", arg1);
        arg.putIntValue("arg2", arg2);
        msg.setPacMap(arg);
        msg.object = obj;
        return msg;
    }

    // ===tool functions===//
    private boolean isStringEqual(String a, String b) {
        if (a == null) {
            if (b == null) {
                return true;
            } else {
                return false;
            }
        } else {
            return a.equals(b);
        }
    }

    private int sp2px(Context context, float spValue) {
        final float fontScale = DisplayManager.getInstance().getDefaultDisplay(context).get()
            .getAttributes().scalDensity;
        return (int) (spValue * fontScale + 0.5f);
    }

    private int dp2px(Context context, float dpValue) {
        final float densityPixels = DisplayManager.getInstance().getDefaultDisplay(context).get()
            .getAttributes().densityPixels;
        return (int) (dpValue * densityPixels + 0.5f);
    }

    private int getEvaluateColor(float fraction, int startColor, int endColor) {
        int a, r, g, b;

        int sA = (startColor & 0xff000000) >>> 24;
        int sR = (startColor & 0x00ff0000) >>> 16;
        int sG = (startColor & 0x0000ff00) >>> 8;
        int sB = (startColor & 0x000000ff) >>> 0;

        int eA = (endColor & 0xff000000) >>> 24;
        int eR = (endColor & 0x00ff0000) >>> 16;
        int eG = (endColor & 0x0000ff00) >>> 8;
        int eB = (endColor & 0x000000ff) >>> 0;

        a = (int) (sA + (eA - sA) * fraction);
        r = (int) (sR + (eR - sR) * fraction);
        g = (int) (sG + (eG - sG) * fraction);
        b = (int) (sB + (eB - sB) * fraction);

        return a << 24 | r << 16 | g << 8 | b;
    }

    private float getEvaluateSize(float fraction, float startSize, float endSize) {
        return startSize + (endSize - startSize) * fraction;
    }

    private String[] convertCharSequenceArrayToStringArray(CharSequence[] charSequences) {
        if (charSequences == null) {
            return null;
        }
        String[] ret = new String[charSequences.length];
        for (int i = 0; i < charSequences.length; i++) {
            ret[i] = charSequences[i].toString();
        }
        return ret;
    }
}